/**
 * @file ngtcpserver.cpp
 * @author Suk Robert Koh (srkoh87z@gmail.com)
 */

#include "ngine/server/ngtcpserver.h"

#ifdef WIN32
// function prototype
unsigned int WINAPI networkThread(LPVOID lpParam);
#endif

/**
 * Default constructor for NGTcpServer class
 */
NGTcpServer::NGTcpServer(int port, int _maxUser) : NGServer(_maxUser)
{
	// set up network
	if(!setUpNetwork(port))
	{
		/**
		 * TODO: handle error
		 */
	}

	// call worker threads
#ifdef WIN32
	unsigned int id1;
	_beginthreadex(NULL, 0, networkThread, (LPVOID) this, 0, &id1);
#else
	pthread_t id1;
	pthread_create(&id1, NULL, networkThread, (void *) this);
#endif

	NGLogger::debug("NGTcpServer constructor finished");
}

/**
 * Destructor for NGTcpServer class
 */
NGTcpServer::~NGTcpServer()
{
	// clean up
	tearDownNetwork();
}

/**
 * Sets up the network connection for clients
 *
 * @param	int		Port number to listen on
 * @return	BOOL	Whether or not it was successful
 */
BOOL NGTcpServer::setUpNetwork(int port)
{
#ifdef WIN32
	WSADATA wsaData;

	// call WSAStartup
	if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		NGLogger::error("WSAStartup() failed");
		return FALSE;
	}
#endif

	// variables
	SOCKADDR_IN servAddr;

	// create socket
	if((serverSocket = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		NGLogger::error("socket() failed");
		return FALSE;
	}

	maxFd = serverSocket;

	// set socket options
#ifdef WIN32
	char cFlag = 1;
	if(setsockopt(serverSocket, IPPROTO_TCP, TCP_NODELAY, &cFlag, sizeof(char)) == SOCKET_ERROR)
	{
		NGLogger::error("setsockopt() failed");
		return FALSE;
	}
#else
	if(setsockopt(serverSocket, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(int)) == SOCKET_ERROR)
	{
		NGLogger::error("setsockopt() failed");
		return FALSE;
	}
#endif

	// create sockaddr struct
	memset(&servAddr, 0, sizeof(struct sockaddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_port = htons(port);
	servAddr.sin_addr.s_addr = INADDR_ANY;

	// bind
	if(bind(serverSocket, (struct sockaddr *) &servAddr, sizeof(struct sockaddr)) == -1)
	{
		NGLogger::error("bind() failed");
		return FALSE;
	}

	// listen
	if(listen(serverSocket, 5) == -1)
	{
		NGLogger::error("listen() failed");
		return FALSE;
	}
}

/**
 * Tears down network
 */
void NGTcpServer::tearDownNetwork()
{
	// clean up
	closesocket(serverSocket);
#ifdef WIN32
	WSACleanup();
#endif
}

#ifdef WIN32
/**
 * Runs in the background to call process() - for Windows
 *
 * @param	LPVOID		Pointer to this instance
 */
unsigned int WINAPI networkThread(LPVOID lpParam)
{
	NGTcpServer *tmp = (NGTcpServer *) lpParam;

	while(1)
	{
		tmp->process();
	}

	return (DWORD) lpParam;
}
#else
/**
 * Runs in the background to call process() - for Non-Windows
 *
 * @param	void *	Pointer to this instance
 */
void *NGTcpServer::networkThread(void *arg)
{
	NGTcpServer *tmp = (NGTcpServer *) arg;
	while(1)
	{
		tmp->process();
	}
}
#endif

/**
 * Handles multiplexing
 */
BOOL NGTcpServer::process()
{
	fd_set recvSet, sendSet;

	struct timeval tv;
	int i, result;

	// setup variables for select()
	FD_ZERO(&recvSet);
	FD_ZERO(&sendSet);
	FD_SET(serverSocket, &recvSet);

	for(i = 0; i < maxUser; i++)
	{
		if(users[i]->connected)
		{
			// add to recv set
			FD_SET(users[i]->socket, &recvSet);

			// add to send set if there is anything to send
			if(users[i]->sendQueue->size() > 0)
				FD_SET(users[i]->socket, &sendSet);
		}
	}

	// set wait time
	tv.tv_sec = 0;
	tv.tv_usec = 1;

	// call select()
	result = select(maxFd + 1, &recvSet, &sendSet, (fd_set *) 0, &tv);

	if(result > 0)
	{
		// new user connection request
		if(FD_ISSET(serverSocket, &recvSet))
			acceptUser();

		for(i = 0; i < maxUser; i++)
		{
			if(users[i]->connected)
			{
				/**
				 * TODO: figure out whether to use if/else or if/if
				 */
				if(FD_ISSET(users[i]->socket, &recvSet))
				{
					if(!handleRecv(i))
						removeUser(users[i]->userId);
				}
				else if(FD_ISSET(users[i]->socket, &sendSet))
				{
					if(!handleSend(i))
						removeUser(users[i]->userId);
				}
			}
		}
	}
	else if(result < 0)
		return FALSE;

	return TRUE;
}

/**
 * Handles accepting new user
 */
BOOL NGTcpServer::acceptUser()
{
	struct sockaddr_in addr;
	SOCKET socket;
#ifdef WIN32
	int size;
#else
	socklen_t size;
#endif
	size = sizeof(addr);

	NGUser *user;

	// accept
	if((socket = accept(serverSocket, (struct sockaddr *) &addr, &size)) == -1)
		return FALSE;

	NGLogger::info("New user connected: %s, socket: %d", inet_ntoa(addr.sin_addr), socket);

	// get an empty user space
	user = getEmptyUserSpace();

	// user list is full
	if(user == NULL)
	{
		NGLogger::warn("User list is full!");
		closesocket(socket);
		return FALSE;
	}

	// expand the maxFd if necessary
	if(socket > maxFd)
		maxFd = socket;

	// initialize variables for user
	user->connected = TRUE;
	user->socket = socket;
	user->userId = currentUserId;
	user->trafficTick = 0;
	user->trafficCount = 0;
	user->sendQueue->clear();
	user->bufferPosition = 0;

	// send up a message to let application know a user connected
	NGMessage *msg = new NGMessage();
	msg->setSenderId(user->userId);
	msg->setType(NGINE_MSG_CONNECT);
	msg->setData("");
	messageQueue->push(msg);

	currentUserId++;

	return TRUE;
}

/**
 * Handles receiving message
 *
 * @param	int		Position of the user in the users list
 * @return	BOOL	Whether or not it was successful
 */
BOOL NGTcpServer::handleRecv(int arrayPosition)
{
	NGUser *user = users[arrayPosition];

	char buf[DEFAULT_BUF_SIZE];
	int count;

	// users are only allowed to receive certain amount of messages per second
	user->trafficCount++;
	if(NGUtils::currentTime() > user->trafficTick + 1000)
	{
		NGLogger::debug("In traffic check");
		if(user->trafficCount > DEFAULT_MAX_TRAFFIC)
		{
			/**
			 * TODO: decide what to do upon traffic overflow
			 * 				kill the user connection - return FALSE;
			 * 				or just skip out - return TRUE;
			 */
			NGLogger::info("Traffic overflow occured by User %lu.", user->userId);
			return FALSE;
		}
		user->trafficCount = 0;
		user->trafficTick = NGUtils::currentTime();
	}

	// recv
	memset(buf, 0, DEFAULT_BUF_SIZE);
	if((count = recv(user->socket, buf, DEFAULT_BUF_SIZE, 0)) <= 0)
		return FALSE;
	else
	{
		// add to the recvBuffer
		// TODO: decide which one is more efficient: to check message first then put into recvBuffer
		//		 or put into recvBuffer first then check
		memcpy(user->recvBuffer + user->bufferPosition, buf, count);
		user->bufferPosition += count;

		char buffer[DEFAULT_BUF_SIZE];
		WORD length = 0;

		while(user->bufferPosition != 0)
		{
			memset(buffer, 0, DEFAULT_BUF_SIZE);
			memcpy(buffer, user->recvBuffer, messageCoder->getHeaderSize());
			length = messageCoder->getMessageLength(buffer);

			// check if entire message has been received (in terms of size)
			if(user->bufferPosition > length + messageCoder->getHeaderSize())
			{
				memcpy(buffer + messageCoder->getHeaderSize(), user->recvBuffer + messageCoder->getHeaderSize(), length + messageCoder->getTrailerSize());
				NGMessage *msg = messageCoder->decode(buffer, user->userId);

				if(msg == NULL)
				{
					/*
					 * TODO: log that user send in invalid message
					 * TODO: decide whether i'm going to kill the user connection - returning FALSE
					 * 		                or just ignore this message - returning TRUE
					 */
					return TRUE;
				}

				// message is valid, put it into queue
				messageQueue->push(msg);

				if(user->bufferPosition == strlen(msg->getData()) + messageCoder->getHeaderSize() + messageCoder->getTrailerSize())
					user->bufferPosition = 0;
				else
				{
					memcpy(user->recvBuffer, user->recvBuffer + strlen(buffer), user->bufferPosition - strlen(buffer));
					user->bufferPosition -= strlen(buffer);
					user->recvBuffer[user->bufferPosition] = '\0';
				}
			}
			else
				return TRUE;
		}

		memset(user->recvBuffer, 0, DEFAULT_RECV_BUF_SIZE);
		user->bufferPosition = 0;
	}

	return TRUE;
}

/**
 * Handles sending messages
 *
 * @param	in		Position of the user in the users list
 * @return	BOOL	Whether or not it was successful
 *
 * TODO: determine if it can be improved
 */
BOOL NGTcpServer::handleSend(int arrayPosition)
{
	NGUser *user = users[arrayPosition];
	int count;

	// loops until all messages in the queue are sent
	while(user->sendQueue->size() > 0)
	{
		// first peek the message
		NGMessage *msg = user->sendQueue->peek();
		char *sendData = messageCoder->encode(msg);

		// send data and remove message if successful
		count = send(user->socket, sendData, strlen(sendData), 0);
		if(count < 0)
			return FALSE;
		else if(count == strlen(sendData))
			user->sendQueue->remove();

		/**
		 * TODO: consider implementing maximum messages to be sent at each process()
		 */
	}

	return TRUE;
}
